package com.wmx.apachestudy.exec;

import cn.hutool.core.thread.ThreadUtil;
import org.apache.commons.exec.*;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

/**
 * Apache Commons Exec 框架-调用外部程序的瑞士军刀
 * 官网：https://commons.apache.org/proper/commons-exec/tutorial.html
 *
 * @author wangMaoXiong
 * @version 1.0
 * @date 2024/12/22 13:21
 */
public class ExecTest {

    private static final Logger log = LoggerFactory.getLogger(ExecTest.class);

    // 执行结果(0表示成功)：0
    // 输出内容：
    // 正在 Ping www.a.shifen.com [2408:873d:22:1a01:0:ff:b087:eecc] 具有 32 字节的数据:
    // 来自 2408:873d:22:1a01:0:ff:b087:eecc 的回复: 时间=44ms
    // 来自 2408:873d:22:1a01:0:ff:b087:eecc 的回复: 时间=33ms
    // 来自 2408:873d:22:1a01:0:ff:b087:eecc 的回复: 时间=34ms
    // 来自 2408:873d:22:1a01:0:ff:b087:eecc 的回复: 时间=38ms
    //
    // 2408:873d:22:1a01:0:ff:b087:eecc 的 Ping 统计信息:
    //     数据包: 已发送 = 4，已接收 = 4，丢失 = 0 (0% 丢失)，
    // 往返行程的估计时间(以毫秒为单位):
    //     最短 = 33ms，最长 = 44ms，平均 = 37ms
    //
    // 异常信息：
    //
    // 进程已结束，退出代码为 0

    /**
     * 同步/阻塞执行命令
     *
     * @throws IOException /
     */
    @Test
    public void testHelloWorld() throws IOException {
        String line = "ping www.baidu.com";
        // String cmd = "cmd /c dir E:\\wmx";
        // 创建命令行
        CommandLine cmdLine = CommandLine.parse(line);
        // 创建执行器
        DefaultExecutor executor = DefaultExecutor.builder().get();
        // 设置正常退出值，如果进程返回不同的退出值，则认为执行失败，#execute方法将抛出 org.apache.commons.execut.ExecuteException 异常;
        // 不同的程序正常退出值可能不一样;
        executor.setExitValue(0);
        // 使用看门狗设置超时时间
        ExecuteWatchdog watchdog = ExecuteWatchdog.builder().setTimeout(Duration.ofMinutes(1)).get();
        executor.setWatchdog(watchdog);
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
             ByteArrayOutputStream errorStream = new ByteArrayOutputStream()) {
            // 将子进程的标准输出和错误复制到父进程的标准输入和错误。如果输出或错误流设置为空，则来自该流的任何反馈都将丢失。
            PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, errorStream);
            // 设置用于处理输入/输出流的处理器。如果不提供正确的流处理程序，则在写入stdout和stderr时，执行的进程可能会阻塞.
            executor.setStreamHandler(streamHandler);
            // 同步执行命令，阻塞当前线程，一直等到执行完成，或者超时
            int exitValue = executor.execute(cmdLine);
            if (executor.isFailure(exitValue) && watchdog.killedProcess()) {
                log.error("命令执行失败/超时.");
            }
            // 获取执行的输出结果
            String output = outputStream.toString("gbk");
            String error = errorStream.toString("gbk");
            log.info("执行结果(0表示成功)：" + exitValue);
            log.info("输出内容：" + output);
            log.info("异常信息：" + error);
        } catch (ExecuteException e) {
            if (watchdog.killedProcess()) {
                log.error("命令执行超时.", e);
            } else {
                log.error("命令执行失败.", e);
            }
        }
    }

    /**
     * 使用PotPlayer播放器播放视频或者音乐
     *
     * @throws IOException
     */
    @Test
    public void testPotPlayer1() throws IOException {
        // 与原生的API一样，空格默认会被当作分隔符，必须使用双引号，将其作为单个命令行参数处理，而不是将其拆分为多个部分。
        File file = new File("F:\\Music\\富士山下 - 陈奕迅.mp3");
        String line = "D:\\software\\PotPlayer\\PotPlayerMini64.exe \"" + file.getAbsolutePath() + "\"";
        CommandLine cmdLine = CommandLine.parse(line);
        DefaultExecutor executor = DefaultExecutor.builder().get();
        // 同步执行命令，阻塞当前线程，一直等到PotPlayer播放器被手动关闭/PotPlayer进程结束;
        int exitValue = executor.execute(cmdLine);
        log.info("执行结果(0表示成功)：" + exitValue);
    }

    /**
     * 使用PotPlayer播放器播放视频或者音乐
     * 1、当超过指定时间播放器还未被关闭时，将看门狗自动杀死结束.
     *
     * @throws IOException
     */
    @Test
    public void testPotPlayer2() throws IOException {
        File file = new File("F:\\Music\\富士山下 - 陈奕迅.mp3");
        String line = "D:\\software\\PotPlayer\\PotPlayerMini64.exe \"" + file.getAbsolutePath() + "\"";
        CommandLine cmdLine = CommandLine.parse(line);
        log.info("执行命令：" + cmdLine.toString());
        DefaultExecutor executor = DefaultExecutor.builder().get();
        // 看门狗监视器
        ExecuteWatchdog watchdog = ExecuteWatchdog.builder().setTimeout(Duration.ofSeconds(60)).get();
        executor.setWatchdog(watchdog);
        // 同步执行命令，阻塞当前线程，一直等到PotPlayer播放器被手动关闭，或者达到看门狗设置的等待时间后，PotPlayer进程会被自动杀死;
        int exitValue = executor.execute(cmdLine);
        log.info("执行结果(0表示成功)：" + exitValue);
    }

    /**
     * 推荐使用addArgument()方法以增量方式构建命令行，不推荐使用 CommandLine.parse()方法直接解析整个命令行参数。
     * 1、CommandLine.addArgument() 方法可以自动处理参数中的特殊字符，按顺序添加参数，不用担心空格等问题;
     * 2、参数可以直接设置，也可以使用${xxx}从substitutionMap中取值;
     *
     * @throws IOException
     */
    @Test
    public void testPotPlayer3() throws IOException {
        Map<String, Object> substitutionMap = new HashMap();
        substitutionMap.put("file", new File("F:\\Music\\富士山下 - 陈奕迅.mp3"));
        CommandLine cmdLine = CommandLine.parse("D:\\software\\PotPlayer\\PotPlayerMini64.exe");
        cmdLine.addArgument("${file}");
        cmdLine.addArgument("/autoplay");
        cmdLine.addArgument("/seek=0:0:0.0");
        cmdLine.addArgument("/cap");
        cmdLine.setSubstitutionMap(substitutionMap);
        // 执行命令：[D:\software\PotPlayer\PotPlayerMini64.exe, "F:\Music\富士山下 - 陈奕迅.mp3", /autoplay, /seek=0:0:0.0, /cap]
        log.info("执行命令：" + cmdLine.toString());
        DefaultExecutor executor = DefaultExecutor.builder().get();
        // 同步执行命令，阻塞当前线程，一直等到PotPlayer播放器被手动关闭，或者达到看门狗设置的等待时间后，PotPlayer进程会被自动杀死;
        int exitValue = executor.execute(cmdLine);
        log.info("执行结果(0表示成功)：" + exitValue);
    }

    /**
     * 异步执行子进程
     *
     * @throws IOException
     * @throws InterruptedException
     */
    @Test
    public void testPotPlayer4() throws IOException, InterruptedException {
        Map<String, Object> substitutionMap = new HashMap();
        substitutionMap.put("file", new File("F:\\Music\\富士山下 - 陈奕迅.mp3"));
        CommandLine cmdLine = CommandLine.parse("D:\\software\\PotPlayer\\PotPlayerMini64.exe");
        cmdLine.addArgument("${file}");
        cmdLine.addArgument("/autoplay");
        cmdLine.addArgument("/seek=0:0:0.0");
        cmdLine.addArgument("/cap");
        cmdLine.setSubstitutionMap(substitutionMap);
        // 执行命令：[D:\software\PotPlayer\PotPlayerMini64.exe, "F:\Music\富士山下 - 陈奕迅.mp3", /autoplay, /seek=0:0:0.0, /cap]
        log.info("执行命令：" + cmdLine.toString());

        DefaultExecutor executor = DefaultExecutor.builder().get();
        // 异步执行.
        DefaultExecuteResultHandler resultHandler = new DefaultExecuteResultHandler();
        ExecuteWatchdog watchdog = ExecuteWatchdog.builder().setTimeout(Duration.ofSeconds(60)).get();
        executor.setExitValue(0);
        executor.setWatchdog(watchdog);
        // 此方法将不再阻塞.
        executor.execute(cmdLine, resultHandler);
        log.info("子进程正在执行,主线程可以做其它事情.");

        // 主线程强制等待子线程,主线程会被阻塞. 当PotPlayer被手动关闭，或者达到看门狗超时时间，子线程被强制杀死，此方法将不再阻塞.
        resultHandler.waitFor();
        int exitValue = resultHandler.getExitValue();
        log.info("子进程执行结果(0表示成功,1表示超时)：" + exitValue);

        // 继续执行其他任务...
        log.info("主线程结束.");
    }

    /**
     * 使用看门狗主动结束子进程
     *
     * @throws IOException
     */
    @Test
    public void testPotPlayer5() throws IOException {
        Map<String, Object> substitutionMap = new HashMap();
        substitutionMap.put("file", new File("F:\\Music\\富士山下 - 陈奕迅.mp3"));
        CommandLine cmdLine = CommandLine.parse("D:\\software\\PotPlayer\\PotPlayerMini64.exe");
        cmdLine.addArgument("${file}");
        cmdLine.addArgument("/autoplay");
        cmdLine.addArgument("/seek=0:0:0.0");
        cmdLine.addArgument("/cap");
        cmdLine.setSubstitutionMap(substitutionMap);
        // 执行命令：[D:\software\PotPlayer\PotPlayerMini64.exe, "F:\Music\富士山下 - 陈奕迅.mp3", /autoplay, /seek=0:0:0.0, /cap]
        log.info("执行命令：" + cmdLine.toString());

        DefaultExecutor executor = DefaultExecutor.builder().get();
        // 异步执行.
        DefaultExecuteResultHandler resultHandler = new DefaultExecuteResultHandler();
        ExecuteWatchdog watchdog = ExecuteWatchdog.builder().setTimeout(Duration.ofSeconds(60)).get();
        executor.setExitValue(0);
        executor.setWatchdog(watchdog);
        // 此方法将不再阻塞.
        executor.execute(cmdLine, resultHandler);
        log.info("子进程正在执行,主线程可以做其它事情.");

        for (int i = 30; i > 0; i--) {
            log.info(i + "秒后强制结束子进程.");
            ThreadUtil.safeSleep(1000);
        }
        // 主动销毁正在运行的子进程。
        watchdog.destroyProcess();

        // 继续执行其他任务...
        log.info("主线程结束.");
    }

    /**
     * 异步执行
     *
     * @throws Exception /
     */
    @Test
    public void testAsyncExecute() throws Exception {
        File file = new File("F:\\Music\\富士山下 - 陈奕迅.mp3");
        String line = "D:\\software\\PotPlayer\\PotPlayerMini64.exe \"" + file.getAbsolutePath() + "\"";
        CommandLine cmdLine = CommandLine.parse(line);
        log.info("执行命令：" + cmdLine.toString());

        DefaultExecutor executor = DefaultExecutor.builder().get();
        // 异步执行(不会阻塞当前线程)
        ExecuteResultHandler handler = new DefaultExecuteResultHandler();
        executor.execute(cmdLine, handler);

        // 继续执行其他任务...
        log.info("主线程先走了.");
        Scanner scan = new Scanner(System.in);
        String nextWord = scan.nextLine();
        log.info("程序退出." + nextWord);
    }

    // 小技巧
    // 工作目录设置：通过executor.setWorkingDirectory()指定命令执行的目录
    // 错误处理：可以通过继承DefaultExecuteResultHandler自定义错误处理逻辑

}